DataLiberation: add a streaming ShortcodeProcessor tokenizer - #300
Open
adamziel wants to merge 6 commits into
Open
DataLiberation: add a streaming ShortcodeProcessor tokenizer#300adamziel wants to merge 6 commits into
adamziel wants to merge 6 commits into
Conversation
Builder content can interleave native WordPress shortcodes with HTML, CSS, JSON attributes, block markup, and third-party shortcodes. Passing the entire value through an HTML serializer can escape bytes belonging to those other grammars. Add a pull-based tokenizer that reports shortcode and text tokens, records source byte spans, and applies queued attribute or text updates without reserializing unrelated input. Opening and closing tokens remain independent, so same-name nesting does not depend on Core's enclosing shortcode regular expression. Cover Divi, WPBakery, Avada, Themify, Oxygen, Gutenberg, Elementor, Beaver Builder, and SiteOrigin shapes. Include mixed-region, malformed, ambiguous bracket syntax, nested shortcode, CSS URL, and encoded UTF-8 cases.
adamziel
force-pushed
the
adamziel/trace-css-corruption
branch
from
July 30, 2026 20:34
0d31a0d to
e12ef85
Compare
Route shortcode-bearing HTML text nodes through ShortcodeProcessor before inspecting direct URL and CSS attribute values. Preserve exact source bytes when applying nested updates so ampersands and builder CSS are not HTML-encoded.\n\nMove the mixed markup coverage out of the tokenizer unit suite and into a focused BlockMarkupUrlProcessor integration suite.
adamziel
added a commit
to WordPress/reprint
that referenced
this pull request
Sep 2, 2026
WPBakery pages now migrate URLs inside shortcode attributes and encoded
payloads while preserving the surrounding shortcode syntax and WPBakery
storage format.
## Why
WPBakery can hide markup behind more than one encoding layer. For
example:
```text
[vc_table allow_html="1"]Download,%3Ca%20href%3D%22https%3A%2F%2Fold.example%2Fmanual.pdf%22%3ELink%3C%2Fa%3E[/vc_table]
```
The URL belongs to an HTML link, but the HTML is URL-encoded inside an
Easy Tables cell. The block-markup processor cannot see that link.
Passing the complete shortcode through an HTML serializer is also unsafe
because shortcode attributes may themselves contain HTML, JSON, CSS,
quotes, and brackets whose original bytes must stay intact.
## What changes
This gives the structured-data rewriter an explicit parsing order:
```text
serialized PHP, JSON, or Base64
-> shortcode syntax
-> generic shortcode attributes
-> registered builder codecs
-> HTML and block markup
```
Shortcode parsing runs first only for leaves already known to contain
block markup. Generic attribute values use the existing cautious URL
scanner, so URLs can move without reserializing the shortcode or
changing its delimiters. The shortcode parser comes from
[WordPress/php-toolkit#300](WordPress/php-toolkit#300);
Reprint supplies the class until a released toolkit dependency includes
it.
Builder-specific formats use a tag-and-attribute registry.
`StructuredDataUrlRewriter` handles generic shortcode traversal and
dispatch. `WPBakeryUrlRewriter` supplies the WPBakery tag registry and
storage codecs. Each codec decodes its value, sends the decoded value
back through the structured rewriter, and then restores the original
storage layers. The first registered WPBakery formats are:
- URL-encoded cells in `vc_table`, including row, column, and style
delimiters;
- Base64 and Base64-plus-URL-encoded bodies in `vc_raw_html` and
`vc_raw_js`;
- pipe-delimited `vc_link` fields used by buttons, headings, icons,
images, and grid items;
- `#E-8_` Base64-plus-URL-encoded fields used by maps, galleries,
carousels, and post sliders.
This recursive path covers combinations such as SQL Base64 -> shortcode
-> WPBakery Base64 -> URL encoding -> HTML -> JSON, CSS, or JavaScript.
JSON bodies in `<script>` elements are parsed as JSON when `type` is
`application/json` or an `application/*+json` media type. Other script
bodies keep the cautious byte scan.
A registered body codec runs only for a complete text body between a
matching opening and closing shortcode. Unknown encoded-body formats and
incomplete body spans stay opaque.
## Known limits
Three cases remain recorded as incomplete tests:
- a URL written as a JavaScript regular-expression literal;
- a URL hidden behind two layers of HTML character references;
- a percent-encoded URL inside an otherwise literal Raw HTML body, which
the current WPBakery codec mistakes for a URL-encoded whole body.
## Verification
An import of the supplied WPBakery test site's database left 12
source-host references in the homepage content on trunk. This branch
rewrote 11 and left only the JavaScript regular-expression case above.
The unit suite checks exact stored output across SQL Base64 alignments,
shortcode quoting and brackets, table delimiters, both Raw HTML
encodings, Raw JS, WPBakery attribute formats, JSON slash and Unicode
escapes, HTML character references, CSS escapes, valid percent escapes
in URL paths and queries, and malformed or unknown shortcode bodies. The
E2E test checks both the stored shortcode and its rendered URL.
## Rewriter benchmark
Each page benchmark rewrites 10,000 distinct pages of about 17 KB. Input
construction is outside the timer. Each number is the median of three
fresh PHP processes.
| Page corpus | trunk | this branch | time change | peak memory, trunk
-> branch |
| --- | ---: | ---: | ---: | ---: |
| Ordinary Gutenberg markup | 25.07 s | 25.43 s | +1.5% | 44 -> 46 MiB |
| Generic non-WPBakery shortcode | 21.42 s | 21.79 s | +1.7% | 42 -> 42
MiB |
| Complex WPBakery homepage | 58.45 s | 77.15 s | +32.0% | 42 -> 48 MiB
|
Both non-WPBakery corpora rewrote all 10,000 direct source URLs. The
WPBakery corpus contains 13 source references inside each decoded Raw
HTML body: trunk rewrote none, while this branch rewrote 12. The
remaining reference is the known JavaScript regular-expression case.
The SQL rewriter owns the Base64 fast-reject. It checks the four
fragments that cover lowercase `http` and `https` at every byte
alignment before looking for encoded builder markers derived from the
structured rewriter's decoded shortcode prefixes. On a URL-free 15.5 MB
`INSERT`, trunk returned in 15.7 ms and this branch returned in 31.4 ms,
before SQL tokenization or Base64 decoding. Combining the 21
builder-marker searches into one pattern reduced the branch result from
77.1 ms.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Introduces
ShortcodeProcessor, a pull-based tokenizer and minimal editor for native WordPress shortcode markup.Data Liberation needs to rewrite URLs without rendering content or changing unrelated bytes. Builder content can contain several nested languages:
Treating the complete value as HTML can turn the CSS into
content: "<";. Treating it as undifferentiated text cannot reliably separate a URL from adjacent CSS syntax. Runningdo_shortcode()is also unsuitable during an import: the original callbacks may be unavailable, and rendering discards the stored representation being migrated.ShortcodeProcessorsupplies the outer shortcode layer. It identifies shortcode and text regions, exposes shortcode attributes without normalizing them, and applies replacements to their original byte spans.Architecture
The processor follows the restricted, forward-only model of
WP_HTML_Tag_Processor:next_token()advances through#shortcodeand#texttokens.next_shortcode()skips text and optionally filters by exact tag, tag prefix, closer policy, match offset, or escaped status.get_updated_text()applies those replacements while copying every unrelated byte unchanged.It does not build a tree, invoke callbacks, or normalize the document. Opening and closing shortcodes are independent tokens, so same-name nesting remains visible:
The public API provides token location and text accessors; shortcode tag, closer, self-closing, and escaped state; attribute iteration, lookup, and replacement; and raw text-token replacement. It can update an existing attribute value or a complete text token, but does not add or remove attributes.
Where it applies
The class tokenizes a shortcode-bearing region. It does not guess whether an entire database field is HTML, CSS, JSON, serialized PHP, blocks, or shortcodes.
Divi's
custom_css_*attributes illustrate the composition model:This preserves
content: "<", the closing)inurl(...), and declarations following the URL. Standalone Customizer or theme-option CSS should go directly toCSSURLProcessor; no shortcode pass is needed.Parsing boundary
The processor finds shortcode candidates without WordPress or a registered shortcode table. Unlike Core's
get_shortcode_regex(), it reports individual opener, closer, self-closing, and[[escaped]]tokens rather than matching a registered enclosing shortcode.Quoted values may contain CSS, HTML, JSON, URLs, square brackets, and shortcode-like text. Named lookup is ASCII case-insensitive, positional attributes remain iterable, and the last duplicate named attribute wins. U+00A0 NO-BREAK SPACE and U+200B ZERO WIDTH SPACE are recognized as separators without changing the source.
[hidden]remains inherently ambiguous: it may be a shortcode, CSS selector, BBCode, or prose. Callers must isolate a shortcode-bearing region and should filter by registered names or known prefixes such aset_pb_,vc_,fusion_, orct_.Attribute updates preserve the existing delimiter when safe and switch delimiters when possible. If a new value requires quoting but contains both quote characters, the update returns
falserather than emitting malformed markup.Alternatives
do_shortcode()renders the content, runs plugin code, and destroys the builder representation.This class is not a sanitizer. Candidate matching does not prove that a tag is registered, and URL validation remains the URL-rewrite layer's responsibility.
Testing
The 35 focused tests and 94 assertions cover:
content: "<",url(...) no-repeat, and percent-encoded UTF-8 paths.The complete Data Liberation suite passes with 2,400 tests and 12,026 assertions. CI passes on PHP 7.2 through 8.5 across Linux, macOS, and Windows.
Follow-up
This PR supplies the tokenizer only. After it is merged and released, Reprint can classify fields from storage identity, plugin/post-meta signals, block markup, and known shortcode prefixes; decode structured containers; and dispatch each selected region to its owning processor.
Related: original
WP_HTML_Tag_Processorproposal,CSSURLProcessor, and URLInTextProcessor punctuation handling.